﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.IO;
using System.Xml.Serialization;
using System.Reflection;
using System.Collections;

namespace GameLib.Utils
{
	/// <summary>
	/// A generic class used to serialize objects.
	/// </summary>
	public class GenericSerializer
	{
		static GenericSerializer()
		{
			Console.WriteLine("GenericSerializer constructor");
			Type stringConverterBase = typeof(StringConverter);
			foreach (Type type in stringConverterBase.Assembly.GetTypes())
			{
				if (type.IsSubclassOf(stringConverterBase))
				{
					RegisterStringConverter(type, true);
				}
			}
			/*
			RegisterConverter(typeof(string), delegate(string value)
			{
				return value;
			});

			RegisterConverter(typeof(int), delegate(string value)
			{
				return Convert.ToInt32(value);
			});

			RegisterConverter(typeof(bool), delegate(string value)
			{
				return Convert.ToBoolean(value);
			});

			RegisterConverter(typeof(float), delegate(string value)
			{
				return Convert.ToSingle(value);
			});

			RegisterConverter(typeof(double), delegate(string value)
			{
				return Convert.ToDouble(value);
			});

			RegisterConverter(typeof(DateTime), delegate(string value)
			{
				return Convert.ToDateTime(value);
			});*/

		}

		/// <summary>
		/// Serializes the given object.
		/// </summary>
		/// <typeparam name="T">The type of the object to be serialized.</typeparam>
		/// <param name="obj">The object to be serialized.</param>
		/// <returns>String representation of the serialized object.</returns>
		public static string Serialize<T>(T obj)
		{
			XmlSerializer xs = null;
			StringWriter sw = null;
			try
			{
				xs = new XmlSerializer(typeof(T));
				sw = new StringWriter();
				xs.Serialize(sw, obj);
				sw.Flush();
				return sw.ToString();
			}
			catch (Exception ex)
			{
				throw ex;
			}
			finally
			{
				if (sw != null)
				{
					sw.Close();
					sw.Dispose();
				}
			}
		}
		public static string Serialize<T>(T obj, Type[] extraTypes)
		{
			XmlSerializer xs = null;
			StringWriter sw = null;
			try
			{
				xs = new XmlSerializer(typeof(T), extraTypes);
				sw = new StringWriter();
				xs.Serialize(sw, obj);
				sw.Flush();
				return sw.ToString();
			}
			catch (Exception ex)
			{
				throw ex;
			}
			finally
			{
				if (sw != null)
				{
					sw.Close();
					sw.Dispose();
				}
			}
		}
		/// <summary>
		/// Serializes the given object.
		/// </summary>
		/// <typeparam name="T">The type of the object to be serialized.</typeparam>
		/// <param name="obj">The object to be serialized.</param>
		/// <param name="writer">The writer to be used for output in the serialization.</param>
		public static void Serialize<T>(T obj, XmlWriter writer)
		{
			XmlSerializer xs = new XmlSerializer(typeof(T));
			xs.Serialize(writer, obj);
		}
		/// <summary>
		/// Serializes the given object.
		/// </summary>
		/// <typeparam name="T">The type of the object to be serialized.</typeparam>
		/// <param name="obj">The object to be serialized.</param>
		/// <param name="writer">The writer to be used for output in the serialization.</param>
		/// <param name="extraTypes"><c>Type</c> array
		///       of additional object types to serialize.</param>
		public static void Serialize<T>(T obj, XmlWriter writer, Type[] extraTypes)
		{
			XmlSerializer xs = new XmlSerializer(typeof(T), extraTypes);
			xs.Serialize(writer, obj);
		}
		/// <summary>
		/// Deserializes the given object.
		/// </summary>
		/// <typeparam name="T">The type of the object to be deserialized.</typeparam>
		/// <param name="reader">The reader used to retrieve the serialized object.</param>
		/// <returns>The deserialized object of type T.</returns>
		private static T Deserialize<T>(XmlReader reader)
		{
			Type type = typeof(T);
			
			// instance
			object obj = type.Assembly.CreateInstance(type.ToString());

			Deserialize(obj, reader);

			return (T)obj;
		}
		/// <summary>
		/// Deserializes the given object.
		/// </summary>
		/// <typeparam name="T">The type of the object to be deserialized.</typeparam>
		/// <param name="reader">The reader used to retrieve the serialized object.</param>
		/// <param name="extraTypes"><c>Type</c> array
		///           of additional object types to deserialize.</param>
		/// <returns>The deserialized object of type T.</returns>
		public static T Deserialize<T>(XmlReader reader, Type[] extraTypes)
		{
			XmlSerializer xs = new XmlSerializer(typeof(T), extraTypes);
			return (T)xs.Deserialize(reader);
		}


		private static object ConvertFromString(string value, Type type)
		{
			Type converterType;

			if (type == typeof(string))
			{
				return value;
			}
			else if (ms_StringConverterDict.TryGetValue(type, out converterType))
			{
				MethodInfo mi = converterType.GetMethod("ConvertFrom");				
				return mi.Invoke(null, new object[]{value});
			}
			else
			{
				// customized type, must use IStringConvertable interface
				object newObj = type.Assembly.CreateInstance(type.ToString());

				IStringConvertable conv = newObj as IStringConvertable;

				if (conv != null)
				{
					conv.FromString(value);		
					return newObj;
				}
			}
			
			return null;
		}

		/*
		private static bool TryConvertFromString(XmlReader reader, Type type, out object value)
		{
			value = null;

			if (reader.IsEmptyElement)
				return false;

			string stringValue = reader.ReadElementString();

			if (stringValue == "")
				return false;

			value = ConvertFromString(stringValue, type);

			//reader.ReadEndElement(); // read out the rest of the child element
						
			return true;
		}*/

		private static bool IsStringConvertable(Type type)
		{
			if (type == typeof(string))
				return true;

			if (ms_StringConverterDict.ContainsKey(type))
				return true;

			if (type.GetInterface("IStringConvertable") != null)
				return true;

			return false;
		}

		private static bool IsSubObjectType(Type type)
		{
			return !IsStringConvertable(type);
		}



		/// <summary>
		/// Deserializes the given object.
		/// </summary>
		/// <typeparam name="T">The type of the object to be deserialized.</typeparam>
		/// <param name="XML">The XML file containing the serialized object.</param>
		/// <returns>The deserialized object of type T.</returns>
		public static T Deserialize<T>(string XML)
		{
			if (XML == null || XML == string.Empty)
				return default(T);
			XmlSerializer xs = null;
			StringReader sr = null;
			try
			{
				xs = new XmlSerializer(typeof(T));
				sr = new StringReader(XML);

				return (T)xs.Deserialize(sr);
			}
			catch (Exception ex)
			{
				throw ex;
			}
			finally
			{
				if (sr != null)
				{
					sr.Close();
					sr.Dispose();
				}
			}
		}
		public static T Deserialize<T>(string XML, Type[] extraTypes)
		{
			if (XML == null || XML == string.Empty)
				return default(T);
			XmlSerializer xs = null;
			StringReader sr = null;
			try
			{
				xs = new XmlSerializer(typeof(T), extraTypes);
				sr = new StringReader(XML);
				return (T)xs.Deserialize(sr);
			}
			catch (Exception ex)
			{
				throw ex;
			}
			finally
			{
				if (sr != null)
				{
					sr.Close();
					sr.Dispose();
				}
			}
		}
		public static void SaveAs<T>(T Obj, string FileName,
						   Encoding encoding, Type[] extraTypes)
		{
			if (File.Exists(FileName))
				File.Delete(FileName);
			DirectoryInfo di = new DirectoryInfo(Path.GetDirectoryName(FileName));
			if (!di.Exists)
				di.Create();
			XmlDocument document = new XmlDocument();
			XmlWriterSettings wSettings = new XmlWriterSettings();
			wSettings.Indent = true;
			wSettings.Encoding = encoding;
			wSettings.CloseOutput = true;
			wSettings.CheckCharacters = false;
			using (XmlWriter writer = XmlWriter.Create(FileName, wSettings))
			{
				if (extraTypes != null)
					Serialize<T>(Obj, writer, extraTypes);
				else
					Serialize<T>(Obj, writer);
				writer.Flush();
				document.Save(writer);
			}
		}
		public static void SaveAs<T>(T Obj, string FileName, Type[] extraTypes)
		{
			SaveAs<T>(Obj, FileName, Encoding.UTF8, extraTypes);
		}
		public static void SaveAs<T>(T Obj, string FileName, Encoding encoding)
		{
			SaveAs<T>(Obj, FileName, encoding, null);
		}
		public static void SaveAs<T>(T Obj, string FileName)
		{
			SaveAs<T>(Obj, FileName, Encoding.UTF8);
		}
		public static T Open<T>(string FileName, Type[] extraTypes)
		{
			T obj = default(T);
			if (File.Exists(FileName))
			{
				XmlReaderSettings rSettings = new XmlReaderSettings();
				rSettings.CloseInput = true;
				rSettings.CheckCharacters = false;
				using (XmlReader reader = XmlReader.Create(FileName, rSettings))
				{
					reader.ReadOuterXml();
					if (extraTypes != null)
						obj = Deserialize<T>(reader, extraTypes);
					else
						obj = Deserialize<T>(reader);
				}
			}
			return obj;
		}
		public static T Open<T>(string FileName)
		{
			return Open<T>(FileName, null);
		}


		protected static object Load<T>(string fileName, object obj)
		{
			if (File.Exists(fileName))
			{
				XmlReaderSettings rSettings = new XmlReaderSettings();
				rSettings.CloseInput = true;
				rSettings.CheckCharacters = false;
				using (XmlReader reader = XmlReader.Create(fileName, rSettings))
				{
					reader.ReadOuterXml();
					reader.MoveToContent();
					if (obj != null)
						Deserialize(obj, reader);
					else
						obj = Deserialize<T>(reader);
				}
			}

			if (obj == null)
				obj = default(T);

			return obj;
		}

		public static object Load<T>(string fileName)
		{
			return Load<T>(fileName, null);
		}

		public static void Load(string fileName, object obj)
		{
			if (obj != null)
				Load<object>(fileName, obj);
		}

		public static void Deserialize(object obj, XmlReader reader)
		{
			if (IsDictionary(obj))
			{
				DeserializeDictionary(obj, reader);
			}
			else if (IsCollection(obj))
			{
				DeserializeCollection(obj, reader);
			}
			else
			{
				DeserializeDefaultObject(obj, reader);
			}
		}

		private static bool IsCollection(object obj)
		{
			return obj.GetType().GetInterface("ICollection`1") != null;
		}

		private static void DeserializeCollection(object obj, XmlReader reader)
		{
			//ICollection collection = obj as ICollection;

			//if (collection == null)
			//	return;

			if (reader.NodeType != XmlNodeType.Element || reader.IsEmptyElement)
			{
				reader.Skip();
				return;
			}

			Type objType = obj.GetType();
			// grab the IDicitonary<> interface
			Type collectionType = objType.GetInterface("ICollection`1");

			Type itemType = typeof(object);

			if (collectionType != null)
			{
				Type[] itemTypes = collectionType.GetGenericArguments();
				itemType = itemTypes[0];
			}

			Type[] mtsAdd = { itemType };
			MethodInfo miAdd = obj.GetType().GetMethod("Add", mtsAdd);


			reader.ReadStartElement();

			do
			{
				if (reader.IsStartElement())
				{
					// read sub object
					string elemName = reader.Name;

					// attempt to use the type indicated by the element name
					Type typeFromName = GetType(elemName, itemType);

					if (typeFromName != null)
						itemType = typeFromName;

					// instance the object
					object item = itemType.Assembly.CreateInstance(itemType.ToString());
					Deserialize(item, reader);

					object[] parms = { item };
					miAdd.Invoke(obj, parms);					
				}
				else
				{
					reader.Read();
				}

				reader.MoveToContent();

			} while (reader.NodeType != XmlNodeType.EndElement && reader.NodeType != XmlNodeType.None);

			reader.ReadEndElement();
		}

		private static bool IsDictionary(object obj)
		{
			return obj is IDictionary;
		}


		private static void DeserializeDictionary(object obj, XmlReader reader)
		{
			IDictionary dict = obj as IDictionary;

			if (dict == null)
				return;

			if (reader.NodeType != XmlNodeType.Element || reader.IsEmptyElement)
			{
				reader.Skip();
				return;
			}

			Type objType = obj.GetType();
			// grab the IDicitonary<> interface
			Type dictType = objType.GetInterface("IDictionary`2");

			Type keyType = typeof(object);
			Type valueType = typeof(object);

			if (dictType != null)
			{
				Type[] keyValueTypes = dictType.GetGenericArguments();
				keyType = keyValueTypes[0];
				valueType = keyValueTypes[1];
			}

			reader.ReadStartElement();

			do
			{
				if (reader.IsStartElement())
				{
					// read sub object
					string keyName, typeName;
					ParseElementName(reader.Name, out keyName, out typeName);

					Type thisValueType = valueType;

					if (typeName != string.Empty)
					{
						thisValueType = GetType(typeName, valueType);
						if (thisValueType == null) // make sure it's the sub type
							thisValueType = valueType;
					}

					object key = ConvertFromString(keyName, keyType);

					// instance the object
					object value = thisValueType.Assembly.CreateInstance(thisValueType.ToString());
					Deserialize(value, reader);

					if (key != null && value != null)
						dict[key] = value;
				}
				else
				{
					reader.Read();
				}
				
				reader.MoveToContent();

			} while (reader.NodeType != XmlNodeType.EndElement && reader.NodeType != XmlNodeType.None);

			// read to next tag
			reader.ReadEndElement();

		}

		public static void DeserializeDefaultObject(object obj, XmlReader reader)
		{
			Type objType = obj.GetType();

			// reader should be positioned at start tag
			if (reader.NodeType == XmlNodeType.Element)
			{
				bool isEmptyElement = reader.IsEmptyElement;

				// read all attributes
				if (reader.HasAttributes)
				{
					while (reader.MoveToNextAttribute())
					{
						PropertyInfo pi = objType.GetProperty(reader.Name);
						if (pi != null && pi.CanWrite)
						{
							pi.SetValue(obj, ConvertFromString(reader.Value, pi.PropertyType), null);
						}

						//Console.WriteLine(" {0}={1}", reader.Name, reader.Value);
					}
				}

				// read to the node after the start tag
				reader.Read();

				if (!isEmptyElement)
				{
					// sub objects
					do
					{
						// move to sub object start tag
						if (reader.IsStartElement())
						{
							// TODO: convert to use ParseElementName
							// parse the name (PropertyName-Type)
							string[] names = reader.Name.Split("-.".ToCharArray());

							string propName = names[0];

							PropertyInfo pi = objType.GetProperty(propName);

							//FieldInfo fi = objType.GetField(propName);

							if (pi == null)
							{
								reader.Skip();
								continue;
							}

							// find out the actual instance type
							Type instType = pi.PropertyType;

							if (names.Length >= 2)
							{
								string typeName = names[1];
								Type type = GetType(typeName, instType);

								if (type != null && type.IsSubclassOf(pi.PropertyType))
									instType = type;
							}

							if (IsSubObjectType(instType))
							{
								object value = pi.GetValue(obj, null);

								if (value != null)
								{
									Deserialize(value, reader);
								}
								else
								{
									value = instType.Assembly.CreateInstance(instType.ToString());
									Deserialize(value, reader);
									pi.SetValue(obj, value, null);
								}
							}
							else
							{
								pi.SetValue(obj, ConvertFromString(reader.ReadInnerXml().Trim(), pi.PropertyType), null);
							}
						}
						else
						{
							reader.Read();
						}
						// now it should be at the node after the object end tag

						reader.MoveToContent();

					} while (reader.NodeType != XmlNodeType.EndElement && reader.NodeType != XmlNodeType.None);

					// read to next tag
					reader.Read();
				}
			}
		}

		private static void ParseElementName(string rawName, out string elemName, out string typeName)
		{
			string[] names = rawName.Split("-.".ToCharArray(), 2);

			elemName = names[0];
			typeName = names.Length >= 2 ? names[1] : "";
		}

		private static Type GetType(string typeName, Type parentType)
		{
			Type type;
			
			if (ms_NameTypeDict.TryGetValue(typeName, out type))
			{
				return type;
			}
			
			type = Type.GetType(typeName);

			if (type == null)
			{
				foreach (Assembly assembly in ms_AdditionalAssemblies)
				{
					type = GetTypeFromAssembly(assembly, typeName, parentType);

					if (type != null)
						break;
				}
			}
			else
			{
				if (!type.IsSubclassOf(parentType))
					type = null;	
			}


			ms_NameTypeDict[typeName] = type;

			return type;
		}

		private static Type GetTypeFromAssembly(Assembly assembly, string typeName, Type parentType)
		{
			Type[] types = assembly.GetTypes();

			foreach (Type type in types)
			{
				if (type.IsSubclassOf(parentType) && type.Name == typeName)
					return type;
			}

			return null;
		}

		/*
		delegate object ConvertFromStringDelegate(string value);
		delegate string ConvertToStringDelegate(object value);

		delegate void DeserializeObjectDelegate(object obj, XmlReader reader);
		delegate void SerializeObjectDelegate(object obj, XmlWriter writer);

		private static void RegisterConverter(Type type, ConvertFromStringDelegate from)
		{
			ms_ConvertFromDict[type] = from;
		}

		private static void RegisterDeserializer(Type type, DeserializeObjectDelegate deserializer)
		{
			ms_ObjectDeserializerDict[type] = deserializer;
		}*/

		protected static void RegisterStringConverter(Type converterType, bool includingSubClasses)
		{
			StringConverterAttribute attr = Attribute.GetCustomAttribute(converterType, typeof(StringConverterAttribute)) as StringConverterAttribute;
						
			Type convertingType = attr.ConvertingType;

			RegisterStringConverter(convertingType, converterType, includingSubClasses);
		}

		protected static void RegisterStringConverter(Type convertingType, Type converterType, bool includingSubClasses)
		{
			// register this type
			ms_StringConverterDict[convertingType] = converterType;

			// and all its sub classes in the assembly
			if (includingSubClasses && !convertingType.IsValueType)
			{
				foreach (Type type in convertingType.Assembly.GetTypes())
				{
					if (type.IsSubclassOf(convertingType))
						ms_StringConverterDict[type] = converterType;
				}
			}
		}

		protected static void RegisterStringConverter(Type convertingType, Type converterType)
		{
			RegisterStringConverter(convertingType, converterType, false);
		}

		protected static void AddAdditionalAssembly(Assembly assembly)
		{
			ms_AdditionalAssemblies.Add(assembly);
		}

		//private static Dictionary<Type, ConvertFromStringDelegate> ms_ConvertFromDict = new Dictionary<Type,ConvertFromStringDelegate>();
		//private static Dictionary<Type, DeserializeObjectDelegate> ms_ObjectDeserializerDict = new Dictionary<Type, DeserializeObjectDelegate>();
		private static Dictionary<Type, Type> ms_StringConverterDict = new Dictionary<Type, Type>();
		private static Dictionary<string, Type> ms_NameTypeDict = new Dictionary<string, Type>();
		private static List<Assembly> ms_AdditionalAssemblies = new List<Assembly>();

	}

	[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)]
	public class SerializableObjectAttribute : Attribute
	{

	}

	public interface IStringConvertable
	{
		void FromString(string value);
	}

	[System.AttributeUsage(System.AttributeTargets.Class)]
	public class StringConverterAttribute : Attribute
	{
		public StringConverterAttribute(Type convertingType)
		{
			ConvertingType = convertingType;
		}

		public Type ConvertingType { get; set; }
	}

	public class StringConverter
	{
		public static object ConvertFrom(string value)
		{
			return null;
		}

		public static string ConvertTo(object value)
		{
			return value.ToString();
		}
	}

	[StringConverter(typeof(int))]
	public class IntStringConverter : StringConverter
	{
		public static object ConvertFrom(string value)
		{
			return Convert.ToInt32(value);
		}
	}

	[StringConverter(typeof(float))]
	public class FloatStringConverter : StringConverter
	{
		public static object ConvertFrom(string value)
		{
			return Convert.ToSingle(value);
		}
	}

	[StringConverter(typeof(bool))]
	public class BoolStringConverter : StringConverter
	{
		public static object ConvertFrom(string value)
		{
			return Convert.ToBoolean(value);
		}
	}

	[StringConverter(typeof(double))]
	public class DoubleStringConverter : StringConverter
	{
		public static object ConvertFrom(string value)
		{
			return Convert.ToDouble(value);
		}
	}

	[StringConverter(typeof(DateTime))]
	public class DateTimeStringConverter : StringConverter
	{
		public static object ConvertFrom(string value)
		{
			return Convert.ToDateTime(value);
		}
	}

	[StringConverter(typeof(Name))]
	public class NameStringConverter : StringConverter
	{
		public static object ConvertFrom(string value)
		{
			return Name.FromString(value);
		}
	}
}
